import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class Node extends Named implements Paintable, Serializable {
	private final Configuration config;
	private final Point2D centerPoint = new Point2D.Double(),
			oldCenterPoint = new Point2D.Double();
	private final Ellipse2D circle = new Ellipse2D.Double();
	private final Set<Arrow> arrows = new HashSet<Arrow>();
//	private boolean selected, hovered, held, indicated, highlighted, relationSelected;
	private double labelAngle;
	
	public Node(double x, double y) {
		config = Configuration.getDefaultConfiguration();
		moveTo(x, y);
		setName();
	}

	public String toString() {
		return "Node " + name + " at (" + (int) centerPoint.getX() + ", "
				+ (int) centerPoint.getY() + ")";
	}

	public Set<Arrow> getArrows() {
		return arrows;
	}

	public Point2D getPoint() {
		return centerPoint;
	}

	public void addArrow(Arrow arrow) {
		arrows.add(arrow);
		update();
		updateNeighbours();
	}

	public double distance(double x, double y) {
		return this.centerPoint.distance(x, y);
	}

	public double distance(Point2D point) {
		return this.centerPoint.distance(point);
	}

	public double distance(Node node) {
		return distance(node.centerPoint);
	}

	public double getAngle(Node node) {
		return Math.atan2(node.centerPoint.getY() - centerPoint.getY(),
				node.centerPoint.getX() - centerPoint.getX());
	}

	public boolean isClose(Point2D point) {
		return distance(point) <= config.nodeRadius;
	}

	public boolean isClose(Node node) {
		return isClose(node.getPoint());
	}

	public void update() {
		for (Arrow arrow : arrows) {
			arrow.update();
		}
		labelAngle = calculateAvailableAngle(null);
	}

	public void moveTo(Point2D point) {
		moveTo(point.getX(), point.getY());
	}

    public void moveTo(double x, double y) {
        centerPoint.setLocation(x, y);
        circle.setFrame(x - config.visibleNodeRadius, y
                - config.visibleNodeRadius, 2 * config.visibleNodeRadius,
                2 * config.visibleNodeRadius);
        update();
        updateNeighbours();
    }

    public void move (double dx, double dy) {
        moveTo(centerPoint.getX() + dx, centerPoint.getY() + dy);
    }

	public void removeArrow(Arrow arrow) {
		arrows.remove(arrow);
		update();
		updateNeighbours();
	}

	private void updateNeighbours() {
		for (Node node : getNeighbours()) {
			node.update();
		}
	}

	public Set<Node> getNeighbours() {
		Set<Node> neighbours = new HashSet<Node>();
		for (Arrow arrow : arrows) {
			neighbours.add(arrow.getToNode());
			neighbours.add(arrow.getFromNode());
		}
		neighbours.remove(this);
		return neighbours;
	}

	public Arrow getLoopArrow() {
		for (Arrow arrow : arrows) {
			if (arrow.isLoop()) {
				return arrow;
			}
		}
		return null;
	}
	
	public void paint(Graphics2D g){
		paint(g, null);
	}
	
	public void paint(Graphics2D g, Representation repr) {
		if (selected) {
			g.setPaint(config.selectedNodeColor);
		} else if (hovered) {
			g.setPaint(config.hoveredNodeColor);
		} else if (held) {
			g.setPaint(config.heldNodeColor);
		} else if (indicated) {
			g.setPaint(config.indicatedNodeColor);
        } else if (highlighted) {
            g.setPaint(config.highlightedNodeColor);
        } else if (relationSelected) {
            g.setPaint(config.relationSelectedNodeColor);
		} else {
			g.setPaint(config.nodeColor);
		}
		g.draw(circle);
		g.fill(circle);
		if (config.showLabels) {
			if (repr != null){
				paintLabel(g, ""+repr.getDimension(this));
			} else{
				paintLabel(g);
			}
		}
	}

	@Override
	protected void calculateLabelPoint(double w, double h) {
		double a = labelAngle, r = config.nodeRadius, ax = centerPoint.getX(), ay = centerPoint
				.getY();
		double x, y;
		double cx = ax + Math.cos(a) * r;
		double cy = ay + Math.sin(a) * r;

		double p = Math.PI / 4;
		a = 2 * Math.PI - a; // angle then increases ccw

		if (a >= 0 && a < p) {
			x = cx;
			y = cy + h / 2 - h / 2 * a / p;
		} else if (a >= p && a < 2 * p) {
			x = cx - w / 2 * (a - p) / p;
			y = cy;
		} else if (a >= 2 * p && a < 3 * p) {
			x = cx - w / 2 - w / 2 * (a - 2 * p) / p;
			y = cy;
		} else if (a >= 3 * p && a < 4 * p) {
			x = cx - w;
			y = cy + h / 2 * (a - 3 * p) / p;
		} else if (a >= 4 * p && a < 5 * p) {
			x = cx - w;
			y = cy + h / 2 + h / 2 * (a - 4 * p) / p;
		} else if (a >= 5 * p && a < 6 * p) {
			x = cx - w + w / 2 * (a - 5 * p) / p;
			y = cy + h;
		} else if (a >= 6 * p && a < 7 * p) {
			x = cx - w / 2 + w / 2 * (a - 6 * p) / p;
			y = cy + h;
		} else {
			x = cx;
			y = cy + h - h / 2 * (a - 7 * p) / p;
		}
		labelPoint.setLocation(x, y-h);
	}

	public void setSelected(boolean selected) {
		this.selected = selected;
	}

	public boolean getSelected() {
		return selected;
	}

	public void setHovered(boolean hovered) {
		this.hovered = hovered;
	}

	public void setHeld(boolean held) {
		if (held && !this.held) {
			oldCenterPoint.setLocation(centerPoint);
		}
		this.held = held;
	}

	public void cancelMove() {
		moveTo(oldCenterPoint);
	}

	public void setIndicated(boolean indicated) {
		this.indicated = indicated;
	}

    public void setHighlighted(boolean highlighted) {
        this.highlighted = highlighted;
    }
    
	public void snapToGrid(double x, double y) {
		moveTo(config.gridSize * Math.round(x / config.gridSize), config.gridSize
                * Math.round(y / config.gridSize));
	}

	public void snapToGrid() {
		snapToGrid(centerPoint.getX(), centerPoint.getY());
	}

	public static double normalizeAngle(double angle) {
		double count = angle / (2 * Math.PI);
		if (angle < 0) {
			angle += Math.ceil(Math.abs(count)) * 2 * Math.PI;
		} else {
			angle -= Math.floor(count) * 2 * Math.PI;
		}
		return angle;
	}

	public double calculateAvailableAngle(Arrow excludeArrow) {
		if (arrows.size() < 1
				|| (arrows.size() == 1 && arrows.contains(excludeArrow))) {
			return 0;
		}
		ArrayList<Double> angles = new ArrayList<Double>();
		double loopAngle1 = 0, loopAngle2 = 0; // we don't want anything to be
		// placed inside the loop
		for (Arrow arrow : arrows) {
			if (arrow == excludeArrow) {
				continue;
			}
			if (arrow.getFromNode() == this) {
				angles.add(normalizeAngle(arrow.getFromAngle()));
			}
			if (arrow.getToNode() == this) {
				angles.add(normalizeAngle(arrow.getToAngle() + Math.PI));
			}
			if (arrow.getFromNode() == arrow.getToNode()) {
				loopAngle1 = normalizeAngle(arrow.getFromAngle());
				loopAngle2 = normalizeAngle(arrow.getToAngle() + Math.PI);
			}
		}
		Collections.sort(angles);
		double currentAngle = 0;
		double maxGap = 0;
		double prevAngle = angles.get(angles.size() - 1);
		prevAngle -= 2 * Math.PI;
		for (double angle : angles) {
			double gap = angle - prevAngle;
			if (gap > maxGap) {
				maxGap = gap;
				// Again some code to avoid placing anything inside a loop.
				// TODO: This doesn't work when loop angle 0.
				double a1 = normalizeAngle(angle), a2 = normalizeAngle(prevAngle);
				if ((a1 != loopAngle1 || a2 != loopAngle2)
						&& (a1 != loopAngle2 || a2 != loopAngle1)) {
					currentAngle = prevAngle + gap / 2;
				}
			}
			prevAngle = angle;
		}
		return normalizeAngle(currentAngle);
	}

	@Override
	public String generateName(String preferredName) {
		if (preferredName == null || preferredName.isEmpty()) {
			preferredName = "a";
		}
		if (preferredName.equals(name)) {
			return name;
		}
		int[] used = new int[26];
		for (String usedName : QuiverFrame.getInstance().getNamePool()) {
			if (usedName.isEmpty()) {
				continue;
			}
			int i = usedName.charAt(0) - 'a';
			if (i >= 0 && i < used.length) {
				used[i]++;
			}
		}
		int j = 0;
		while (true) {
			for (int i = 0; i < used.length; i++) {
				if (used[i] == j) {
					String name = "";
					String chr = Character.toString((char) (i + 'a'));
					for (int k = 0; k < j + 1; k++) {
						name += chr;
					}
					return name;
				}
			}
			j++;
		}
	}
}
